/**
 * Copyright (c) 2009 blurredVision
 * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
 * associated documentation files (the "Software"), to deal in the Software without restriction, 
 * including without limitation the rights to use, copy, modify, merge, publish, distribute, 
 * sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is 
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all copies or 
 * substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT 
 * NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 
 * DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT 
 * OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */
#include "StdAfx.h"
#include "Model.h"
#include "dx/Renderer.h"

namespace directx {


Model::Model()
	: pMesh(NULL), 
	pMeshMaterials(NULL), 
	pMeshTextures(NULL),
	dwNumMaterials(0L)
{
}

Model::Model(const Model& m)
{
}

Model::~Model(void)
{
	release();
}

void Model::release()
{
	DELETE_ARRAY( pMeshMaterials );

	if(pMeshTextures) {
		for(DWORD i = 0; i < dwNumMaterials; ++i) {
			RELEASE( pMeshTextures[i] );
		}
		dwNumMaterials = 0L;
	}

	// Delete the mesh object
	RELEASE( pMesh );
}

void Model::reset()
{
	// Get the d3dDevice -> see DXResource
	DXResource::reset();

	if (m_bLoaded) {
		load(sFilename);
	}
}

void Model::draw()
{
	if ( !m_bLoaded ) {
		coreGetLog()->WriteWarning("Can't draw model. No model file loaded.", __FILE__, __LINE__);
		return;
	}
	
	try {
		for (DWORD i=0; i<dwNumMaterials; ++i) {			
			if (m_bUseIntMats) {
				// Set the material and texture for this subset
				if (pMeshMaterials)
					pd3dDevice->SetMaterial( &(pMeshMaterials[i]) );
			}
			// Draw the mesh subset
			pMesh->DrawSubset( i );
		}
	} catch (...) {
		coreGetLog()->WriteError("Could not draw mesh", __FILE__, __LINE__);
	}
}


bool Model::load(const std::string &filename) 
{
	sFilename = filename;

	ID3DXMesh *tempMesh = NULL;
	LPD3DXBUFFER pD3DXMtrlBuffer = 0;

    // Load the mesh from the specified file
	if( FAILED( D3DXLoadMeshFromX( filename.c_str(), 
									D3DXMESH_SYSTEMMEM, 
									pd3dDevice, 
									NULL, 
									&pD3DXMtrlBuffer, 
									NULL, 
									&dwNumMaterials, 
									&tempMesh ) ))
	{
		coreGetLog()->WriteError("Could not find mesh file", __FILE__, __LINE__);
		return false;
	}

	tempMesh->CloneMesh( D3DXMESH_SYSTEMMEM, 
						 VertexDeclaration, 
						 pd3dDevice, 
						 &pMesh );

	computeNormals();
	computeTangents();
	
//	loadMaterials(pD3DXMtrlBuffer);

//	optimizeMesh();

	RELEASE( tempMesh );
	RELEASE( pD3DXMtrlBuffer );

	m_bLoaded = true;
	return true;
}

bool Model::loadMaterials(LPD3DXBUFFER pD3DXMtrlBuffer)
{
	// We need to extract the material properties and texture names from the pD3DXMtrlBuffer
	D3DXMATERIAL* d3dxMaterials = (D3DXMATERIAL*)pD3DXMtrlBuffer->GetBufferPointer();
	pMeshMaterials = new D3DMATERIAL9[dwNumMaterials];
	if( pMeshMaterials == NULL ) {
		coreGetLog()->WriteError("Initialization of mesh materials failed. Out of memory.", __FILE__, __LINE__);
		return false;
	}

	pMeshTextures  = new LPDIRECT3DTEXTURE9[dwNumMaterials];
	if( pMeshTextures == NULL ) {
		coreGetLog()->WriteError("Initialization of mesh textures failed. Out of memory.", __FILE__, __LINE__);
		return false;
	}

	for( DWORD i=0; i<dwNumMaterials; ++i ) {
		// Copy the material
		pMeshMaterials[i] = d3dxMaterials[i].MatD3D;

		// Set the ambient color for the material (D3DX does not do this)
		pMeshMaterials[i].Ambient = pMeshMaterials[i].Diffuse;

		pMeshTextures[i] = NULL;

		if( d3dxMaterials[i].pTextureFilename != NULL && 
			lstrlen(d3dxMaterials[i].pTextureFilename) > 0 ) {

			// Create the texture
			if( FAILED( D3DXCreateTextureFromFile( pd3dDevice, 
						  d3dxMaterials[i].pTextureFilename, 
						  &pMeshTextures[i] ) ))
			{            
				coreGetLog()->WriteWarning("Could not find texture map", __FILE__, __LINE__);
			}

//			pd3dDevice->SetSamplerState( 0, D3DSAMP_MINFILTER, D3DTEXF_LINEAR );
//			pd3dDevice->SetSamplerState( 0, D3DSAMP_MAGFILTER, D3DTEXF_LINEAR );
		}
	}

	// Done with the material buffer
	RELEASE( pD3DXMtrlBuffer );

	return true;
}

bool Model::optimizeMesh() 
{
	DWORD *rgdwAdjacency = NULL;
	
/*
    // Make sure there are normals which are required for lighting
    if( !(pMesh->GetFVF() & D3DFVF_NORMAL) )
    {
        ID3DXMesh* pTempMesh;
        if (FAILED( pMesh->CloneMeshFVF( pMesh->GetOptions(), 
                                  pMesh->GetFVF() | D3DFVF_NORMAL, 
                                  pd3dDevice, &pTempMesh ) ))
				{
					coreGetLog()->WriteError("Could not Clone Mesh", __FILE__, __LINE__);
					return false;
				}
        if (FAILED( D3DXComputeNormals( pTempMesh, NULL ) )) 
				{
					coreGetLog()->WriteError("Could not compute mesh normals", __FILE__, __LINE__);
					return false;
				}

        DELETE_POINTER( pMesh );
        pMesh = pTempMesh;
    }
*/
    // Optimize the mesh for this graphics card's vertex cache 
    // so when rendering the mesh's triangle list the vertices will 
    // cache hit more often so it won't have to re-execute the vertex shader 
    // on those vertices so it will improve perf.     
    rgdwAdjacency = new DWORD[pMesh->GetNumFaces() * 3];
	if( rgdwAdjacency == NULL ) {
		coreGetLog()->WriteError("Could not create adjacency space. Out of memory", __FILE__, __LINE__);
		return false;
	}
	if (FAILED( pMesh->GenerateAdjacency(1e-6f,rgdwAdjacency) ))
	{
		coreGetLog()->WriteError("Could not generate adjacency", __FILE__, __LINE__);
		return false;
	}

	if (FAILED( pMesh->OptimizeInplace(D3DXMESHOPT_VERTEXCACHE, rgdwAdjacency, NULL, NULL, NULL) ))
	{
		coreGetLog()->WriteError("Could not optimize mesh", __FILE__, __LINE__);
		return false;
	}
    DELETE_ARRAY( rgdwAdjacency );
	return true;
}

bool Model::computeNormals()
{
	MESH_VERTEX *pMeshVertices;
	WORD * pMeshIndices;

	pMesh->LockIndexBuffer(D3DLOCK_NOSYSLOCK, (void**)&pMeshIndices);
	pMesh->LockVertexBuffer(D3DLOCK_NOSYSLOCK, (void**)&pMeshVertices);

	try {

		calculateNormals( pMeshVertices,
						  pMesh->GetNumVertices(),
						  pMeshIndices,
						  pMesh->GetNumFaces() );
	}
	catch(...) {
		coreGetLog()->WriteError("Could not calculate normals", __FILE__, __LINE__);
		return false;
	}
	pMesh->UnlockVertexBuffer();
	pMesh->UnlockIndexBuffer();

	return true;
}

void Model::calculateNormals(MESH_VERTEX *pVertex, int nVertices, const WORD *pTriangle, int nTriangles)
{
	D3DXVECTOR3 *norm = new D3DXVECTOR3[nVertices];

	memset(norm, 0, sizeof(D3DXVECTOR3) * nVertices);

    for (long a = 0; a < nTriangles; ++a) {
        long i1 = pTriangle[0];
        long i2 = pTriangle[1];
        long i3 = pTriangle[2];
        
		// Get the corners of the triangle
		D3DXVECTOR3 v1 = pVertex[i1].position;
        D3DXVECTOR3 v2 = pVertex[i2].position;
		D3DXVECTOR3 v3 = pVertex[i3].position;
        
		// Calculate the vectors
		D3DXVECTOR3 vA = v2 - v1;
		D3DXVECTOR3 vB = v3 - v1;

		// Calculate the cross product to get the normal
		D3DXVECTOR3 vNorm;
		D3DXVec3Cross(&vNorm, &vA, &vB);

		// Normalize if
		D3DXVec3Normalize(&vNorm, &vNorm);

        norm[i1] += vNorm;
		norm[i2] += vNorm;
		norm[i3] += vNorm;

        pTriangle+=3;
    }
    
	for (int i = 0; i < nVertices; ++i)
	{
		D3DXVec3Normalize(&norm[i], &norm[i]);
		pVertex[i].normal = norm[i];
	}

	DELETE_ARRAY( norm );
}

bool Model::computeTangents()
{
	MESH_VERTEX *pMeshVertices;
	WORD *pMeshIndices;

	pMesh->LockIndexBuffer(D3DLOCK_NOSYSLOCK, (void**)&pMeshIndices);
	pMesh->LockVertexBuffer(D3DLOCK_NOSYSLOCK, (void**)&pMeshVertices);

	try {
	
		calculateTangents( pMeshVertices,
						   pMesh->GetNumVertices(),
						   pMeshIndices,
						   pMesh->GetNumFaces() );
	}
	catch(...) {
		coreGetLog()->WriteError("Failed to calculate tangents. Possible Errors - Locking Index/Vertex Bufs, Calc Tangent Algorithm", __FILE__, __LINE__);
		return false;
	}
	pMesh->UnlockVertexBuffer();
	pMesh->UnlockIndexBuffer();

	return true;
}

void Model::calculateTangents( MESH_VERTEX *pVertex, int nVertices, const WORD *pTriangle, int nTriangles)
{
	D3DXVECTOR3 *tan = new D3DXVECTOR3[nVertices];
	D3DXVECTOR3 *bin = new D3DXVECTOR3[nVertices];

	memset(tan, 0, sizeof(D3DXVECTOR3) * nVertices);
	memset(bin, 0, sizeof(D3DXVECTOR3) * nVertices);

	// Calculate tangents and binormals.
	for (int i = 0; i < nTriangles; ++i)
	{
		int i1 = pTriangle[i * 3];
		int i2 = pTriangle[i * 3 + 1];
		int i3 = pTriangle[i * 3 + 2];

		D3DXVECTOR3 v1 = pVertex[i1].position;
		D3DXVECTOR3 v2 = pVertex[i2].position;
		D3DXVECTOR3 v3 = pVertex[i3].position;

		D3DXVECTOR2 w1 = pVertex[i1].tex0;
		D3DXVECTOR2 w2 = pVertex[i2].tex0;
		D3DXVECTOR2 w3 = pVertex[i3].tex0;

		float x1 = v2.x - v1.x;
		float y1 = v2.y - v1.y;
		float z1 = v2.z - v1.z;
		float x2 = v3.x - v1.x;
		float y2 = v3.y - v1.y;
		float z2 = v3.z - v1.z;

		float s1 = w2.x - w1.x;
		float t1 = w2.y - w1.y;
		float s2 = w3.x - w1.x;
		float t2 = w3.y - w1.y;

		float r = 1.0f / (s1 * t2 - s2 * t1);

		D3DXVECTOR3 sdir((t2 * x1 - t1 * x2) * r,
		(t2 * y1 - t1 * y2) * r,
		(t2 * z1 - t1 * z2) * r);

		D3DXVECTOR3 tdir((s1 * x2 - s2 * x1) * r,
		(s1 * y2 - s2 * y1) * r,
		(s1 * z2 - s2 * z1) * r);

		tan[i1] += sdir;
		tan[i2] += sdir;
		tan[i3] += sdir;

		bin[i1] += tdir;
		bin[i2] += tdir;
		bin[i3] += tdir;
	}

	// Save our tangents and binormals.
	for (int i = 0; i < nVertices; ++i)
	{
		D3DXVec3Normalize(&tan[i], &tan[i]);
		pVertex[i].tangent = tan[i];

		D3DXVec3Normalize(&bin[i], &bin[i]);
		pVertex[i].binormal = bin[i];
	}

	DELETE_ARRAY( tan );
	DELETE_ARRAY( bin );
}

} // namespace
